import JSZip from 'jszip';
import PizZip from 'pizzip';
import fs from 'fs/promises';
import { ensureDirSync } from 'fs-extra';
import Docxtemplater from 'docxtemplater';
import { resolve, normalize, join } from 'path';
import expressionParser from 'docxtemplater/expressions.js';
import { convert as libreConvert } from 'libreoffice-convert';
import ImageModule from '@slosarek/docxtemplater-image-module-free';
import { accessSync, readFileSync, writeFileSync, existsSync } from 'fs';

export interface DocxEditorOptions {
  docxTempName: string;
  data: Record<string, any>;
  outputName: string;
  convert?: boolean;
  replace?: {
    origin: RegExp;
    target: string;
  };
}

export interface GenerateFilesOptions {
  templateName: string;
  data: Array<{
    outputName: string;
    data: Record<string, any>;
  }>;
  outputMode: 'OnlyDocx' | 'OnlyPdf' | 'All';
  replace?: DocxEditorOptions['replace'];
}

// 定义类型
interface FileInfoType {
  docx: string;
  pdf?: string; // 可选属性
}

/**
 * @description 根据模板批量生成DOCX文件
 * @param templateName - 模板名称(.docx文件)
 * @param data - 生成文件的数据: [{ outputName: 文件名, data: 数据 }
 * @param convert - 是否转换为PDF, 默认为false
 * @param replace - 替换内容: { origin: 正则表达式, target: 替换内容 }
 * @param delOriginal - 打包时是否删除原始文件, 默认为false
 * @returns 生成的文件路径
 */
export async function generateFiles(options: GenerateFilesOptions): Promise<string> {
  try {
    const { templateName, data, outputMode, replace } = options;

    // 参数验证
    if (!templateName) throw new Error('模板名称不能为空');
    if (!outputMode) throw new Error('输出模式不能为空');
    if (!data || data.length === 0 || typeof data !== 'object') throw new Error('数据格式不正确');

    const convert = outputMode !== 'OnlyDocx';
    // 确保输出目录存在
    const outputDir = './public/output';
    ensureDirSync(outputDir);

    // 并行生成所有文档
    const generationResults = await Promise.all(
      data.map(async (item) => {
        const docOptions: DocxEditorOptions = {
          docxTempName: templateName,
          data: item.data,
          outputName: item.outputName,
          convert,
          replace,
        };
        try {
          const result = await creatDocxFile(docOptions);
          return {
            docx: result?.docx,
            pdf: result?.pdf,
          };
        } catch (error) {
          throw new Error(`生成文件 ${item.outputName} 失败(${error.message})`);
        }
      })
    );

    // 根据outputMode决定需要处理的文件

    // 提取所有生成的文件路径
    const docxFiles = generationResults.map((res) => res.docx);
    const pdfFiles = generationResults.flatMap((res) => (res.pdf ? [res.pdf] : []));

    // 根据outputMode决定需要处理的文件
    let filesToProcess = [
      ...(outputMode !== 'OnlyPdf' ? docxFiles : []),
      ...(outputMode !== 'OnlyDocx' ? pdfFiles : []),
    ];

    // 单文件直接返回
    if (filesToProcess.length <= 1) {
      return '../output/' + filesToProcess[0];
    }

    // 多文件打包为ZIP
    const zipResult = await createZipBlob(filesToProcess, outputDir, true);

    return '../output/' + zipResult.resPath;
  } catch (error) {
    throw new Error(error.message);
  }
}

/**
 * 生成docx文件，可使用convert参数转换为pdf
 * @param docxTempName 模板名称(.docx文件)
 * @param data 生成文件的数据(包含的字段需与模板中一致)
 * @param outputName 输出文件名称
 * @param convert 是否转换为pdf
 * @param replace 替换字符串, {origin: 原始字符串，Regex, target: 替换字符串}
 * @returns 返回生成的文件名对象, {docx: 文件名, pdf?: 文件名}
 */
export async function creatDocxFile(params: DocxEditorOptions): Promise<{ docx: string; pdf?: string }> {
  try {
    const { docxTempName, data, outputName, convert = false, replace } = params;

    // 参数验证
    if (!docxTempName?.endsWith('.docx')) throw new Error('模板无效，必须为.docx文件！');
    if (!outputName) throw new Error('未指定输出文件名！');
    if (!data || typeof data !== 'object') throw new Error('无效的数据格式');

    const outputDir = './public/output';
    const baseDir = './public/template';
    const templatePath = getSecurePath(baseDir, docxTempName);
    ensureDirSync(outputDir);

    // 处理文档模板
    const zip = new PizZip(readFileSync(templatePath));
    const doc = new Docxtemplater(zip, {
      paragraphLoop: true,
      linebreaks: true,
      modules: [
        new ImageModule({
          centered: false,
          getImage(tagValue, tagName, meta) {
            const imagePath = typeof tagValue === 'string' ? tagValue : tagValue.data;
            return readFileSync(getSecurePath('./public/pic', imagePath, false), 'binary');
          },
          getSize(img, tagValue, tagName) {
            if (tagValue.size && tagValue.data) {
              return tagValue.size;
            }
            if (!tagValue.maxSize) {
              return [100, 100];
            }

            const maxWidth = tagValue.maxSize[0];
            const maxHeight = tagValue.maxSize[1];

            // 计算最佳尺寸
            const widthRatio = img.width / maxWidth;
            const heightRatio = img.height / maxHeight;
            const ratio = Math.max(widthRatio, heightRatio, 1);

            return [Math.round(img.width / ratio), Math.round(img.height / ratio)];
          },
          // getProps(img, tagValue, tagName) {   // 免费模块，不支持设置图片属性
          //   // 如果不需要改变给定的tagValue的属性，应该写：return null;
          //   return {
          //     // align: 'right',
          //     // 图片位于文字下方
          //     // valign: 'bottom',
          //     wrap: 'none', // 图片不换行
          //     border: {
          //       color: '#ff0000',
          //       size: 6,
          //       type: 'sng',
          //     },
          //   };
          // },
        }),
      ],
      parser: customerParser,
    });

    doc.render(data);

    // 生成文档内容
    let docxBuffer = doc.getZip().generate({ type: 'nodebuffer' });

    // 执行正则替换（如果需要）
    if (replace?.origin && replace.target) {
      const xmlZip = new PizZip(docxBuffer);
      const xmlContent = xmlZip.files['word/document.xml'].asText();

      /* 调试用
      // 将xmlContent写入到TXT文件
      const txtPath = join(outputDir, `${outputName}.txt`);
      await fs.writeFile(txtPath, xmlContent);

      const re = replace.origin.toString();
      console.log('正则表达式为：', re);
      // 使用正则表达式匹配replace.target并输出匹配到几处
      const matches = [];
      let match;
      while ((match = replace.origin.exec(xmlContent)) !== null) {
        matches.push(match[0]);
      }
      console.log(`匹配到：${matches.length}处， matches`);
      */

      // 使用正则表达式替换图片后面的换行符
      const updatedXmlContent = xmlContent.replace(replace.origin, replace.target);

      // 将修改后的文件重新打包
      xmlZip.file('word/document.xml', updatedXmlContent);
      docxBuffer = xmlZip.generate({ type: 'nodebuffer' });
    }

    // 保存文档
    const docxPath = getSecurePath(outputDir, `${outputName}.docx`, false);
    writeFileSync(docxPath, docxBuffer);

    let fileInfo: FileInfoType = { docx: docxPath.split('/').pop() };
    if (convert) {
      // 转换为PDF
      const pdfPath = getSecurePath(outputDir, `${outputName}.pdf`, false);
      await convertToPdf(docxPath, pdfPath);
      fileInfo.pdf = pdfPath.split('/').pop();
      // const docxContent = readFileSync(docxPath);
      // // 在调用前设置环境变量
      // process.env.LIBREOFFICE_PATH = '/usr/bin/soffice';
      // await new Promise((resolve, reject) => {
      //   libreConvert(docxContent, '.pdf', undefined, (err, pdfBuffer) => {
      //     if (err) reject(err);
      //     else {
      //       writeFileSync(pdfPath, pdfBuffer);
      //       res.push({ pdf: pdfPath.split('/').pop() });
      //       resolve(true);
      //     }
      //   });
      // });
    }
    // const res: { docx: string; pdf?: string }[] = [fileInfo]; // 对应Promise<{ docx: string; pdf?: string }[]>
    return fileInfo;
  } catch (error) {
    throw new Error(`生成报告失败(${error.message})`);
  }
}

/**
 * 转换为PDF
 */
async function convertToPdf(docxPath: string, pdfPath: string): Promise<void> {
  return new Promise((resolve, reject) => {
    process.env.LIBREOFFICE_PATH = '/usr/bin/soffice';
    const docxContent = readFileSync(docxPath);

    libreConvert(docxContent, '.pdf', undefined, (err, pdfBuffer) => {
      if (err) return reject(err);
      writeFileSync(pdfPath, pdfBuffer);
      resolve();
    });
  });
}

/**
 * 生成zip文件（路径与原始文件相同），返回blob对象和文件路径
 * @param filesList 需要打包的文件列表（文件名.文件类型）
 * @param filesPath 文件所在路径
 * @param deleteOriginal 是否删除原始文件
 * @param isBlob 是否返回blob对象, 已废弃
 */
export async function createZipBlob(
  filesList: string[],
  filesPath: string,
  deleteOriginal: boolean = false
): Promise<{ resPath: string }> {
  try {
    const zip = new JSZip();
    const filesToDelete: string[] = [];
    const docxFolder = zip.folder('docx');
    const pdfFolder = zip.folder('pdf');

    // 并行添加文件到ZIP
    await Promise.all(
      filesList.map(async (fileName) => {
        const filePath = getSecurePath(filesPath, fileName);
        const ext = fileName.split('.').pop()?.toLowerCase();
        try {
          const fileData = await fs.readFile(filePath);

          // 根据文件类型添加到不同的文件夹
          if (ext === 'docx' && docxFolder) {
            docxFolder.file(fileName, fileData);
          } else if (ext === 'pdf' && pdfFolder) {
            pdfFolder.file(fileName, fileData);
          } else {
            zip.file(fileName, fileData);
          }

          filesToDelete.push(filePath);
        } catch (error) {
          // console.error(`添加文件到ZIP失败: ${fileName}`, error);
          throw new Error(`添加 ${fileName} 到ZIP失败(${error.message})`);
        }
      })
    );
    // 检查是否有有效文件
    if (Object.keys(zip.files).length === 0) {
      throw new Error('没有有效文件可压缩');
    }

    // 生成ZIP
    const zipBuffer = await zip.generateAsync({
      type: 'nodebuffer',
      compression: 'DEFLATE',
      compressionOptions: { level: 9 },
      streamFiles: true, // 流式处理
    });
    // const blob = isBlob ? new Blob([zipBuffer], { type: 'application/zip' }) : undefined; // 生成 Blob 对象

    // 保存ZIP
    const zipFileName = `Report_${Date.now()}.zip`;
    const zipPath = getSecurePath(filesPath, zipFileName, false);
    writeFileSync(zipPath, zipBuffer);

    // 清理原始文件
    if (deleteOriginal) {
      await deleteFiles(filesToDelete);
    }

    return { resPath: zipPath.toString().split('/').pop() };
  } catch (error) {
    throw new Error(`创建ZIP失败(${error.message})`); // 重新抛出错误
  }
}

//===============================解析器===============================================//
const customerParser = expressionParser.configure({
  filters: {
    // 图片大小解析器
    picSize(input, width, height) {
      return {
        data: input,
        size: [width, height],
      };
    },
    // 上下标的过滤器
    SubSup(text: string) {
      if (typeof text !== 'string') return text;

      // 分割所有标签和文本
      const parts = text.split(/(<sub>|<\/sub>|<sup>|<\/sup>)/);
      let output = '';
      let isSub = false;
      let isSup = false;

      parts.forEach((part) => {
        if (part === '<sub>') {
          isSub = true;
          return;
        }
        if (part === '</sub>') {
          isSub = false;
          return;
        }
        if (part === '<sup>') {
          isSup = true;
          return;
        }
        if (part === '</sup>') {
          isSup = false;
          return;
        }

        if (isSub) {
          output += `<w:r><w:rPr><w:vertAlign w:val="subscript"/></w:rPr><w:t>${escapeXml(part)}</w:t></w:r>`;
        } else if (isSup) {
          output += `<w:r><w:rPr><w:vertAlign w:val="superscript"/></w:rPr><w:t>${escapeXml(part)}</w:t></w:r>`;
        } else if (part.trim() !== '') {
          // 过滤掉空字符串
          output += `<w:r><w:t xml:space="preserve">${escapeXml(part)}</w:t></w:r>`;
        }
      });
      return `<w:p>${output}</w:p>`; // 返回处理后的字符串, rawXML语法必须使用<w:p>标签包裹
    },
  },
});

//===============================辅助函数===============================================//

/**
 * 获取安全的标准路径
 * @param baseDir 基础目录
 * @param fileName 文件名
 * @param isExist 是否检查文件的存在，默认为true
 * @returns 标准化的绝对路径
 * @throws 路径越界或文件不存在时抛出错误
 */
export const getSecurePath = (baseDir: string, fileName: string, isExist: boolean = true): string => {
  // 解析基础目录为绝对路径
  const resolvedBase = resolve(baseDir);
  // 对文件名进行URL解码
  const decodedFileName = decodeURIComponent(fileName);
  // 安全拼接路径（使用join防止路径拼接漏洞）
  const joinedPath = resolve(join(resolvedBase, decodedFileName));

  // 解析为绝对路径并进行规范化处理
  const safePath = normalize(joinedPath);
  // 防止目录遍历攻击
  if (!safePath.startsWith(resolvedBase)) {
    throw new Error('非法路径访问');
  }
  if (isExist) {
    // 检查文件是否存在
    try {
      accessSync(safePath);
    } catch (error) {
      throw new Error(`文件不存在(${error.message})`);
    }
  }

  return safePath;
};

/**
 * 安全删除文件
 * @param filePaths 文件路径列表
 */
export async function deleteFiles(filePaths: string[]) {
  await Promise.all(
    filePaths.map(async (path) => {
      try {
        await fs.unlink(path);
        // console.log(`已删除: ${path}`);
      } catch (delErr) {
        console.error(`删除失败 ${path}:`, delErr);
        throw new Error(`删除 ${path} 失败(${delErr.message})`);
      }
    })
  );
}

/**
 * 格式化字节大小
 * @param bytes 字节大小，单位为字节
 * @param decimals 小数位数，默认为2
 * @returns 格式化后的字符串
 */
export function formatBytes(bytes: number, decimals = 2) {
  if (bytes === 0) return '0 Bytes';
  const k = 1024;
  const dm = decimals < 0 ? 0 : decimals;
  const sizes = ['Bytes', 'KB', 'MB', 'GB'];
  const i = Math.floor(Math.log(bytes) / Math.log(k));
  return `${parseFloat((bytes / Math.pow(k, i)).toFixed(dm))} ${sizes[i]}`;
}

/**
 * 转义XML特殊字符
 * @param unsafe 需要转义的字符串
 * @returns 转义后的字符串
 * */
function escapeXml(unsafe: string): string {
  return unsafe.replace(/[<>&'"]/g, (char) => {
    // 替换特殊字符,包括 < > & ' "
    switch (char) {
      case '<':
        return '&lt;';
      case '>':
        return '&gt;';
      case '&':
        return '&amp;';
      case "'":
        return '&apos;';
      case '"':
        return '&quot;';
      default:
        return char;
    }
  });
}

//===============================其他函数===============================================//

/**
 * base64转Buffer
 * @param base64Data  base64字符串
 * @returns Buffer
 */
export const base64DataURLToBuffer = (base64Data: string) => {
  // 定义正则表达式，用于验证字符串是否符合Base64编码
  const base64Regex = /^data:image\/(png|jpg|jpeg|svg|svg\+xml);base64,/;
  const validBase64 = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/;

  if (!base64Regex.test(base64Data)) {
    return false;
  }
  const stringBase64 = base64Data.replace(base64Regex, '');
  if (!validBase64.test(stringBase64)) {
    throw new Error('Invalid base64 string');
  }

  if (typeof Buffer !== 'undefined' && Buffer.from) {
    return Buffer.from(stringBase64, 'base64');
  }

  // node环境
  if (typeof Buffer !== 'undefined' && Buffer.from) {
    return Buffer.from(stringBase64, 'base64');
  }

  // 浏览器环境
  let binaryString;
  if (typeof window !== 'undefined') {
    binaryString = window.atob(stringBase64);
  } else {
    binaryString = new Buffer(stringBase64, 'base64').toString('binary');
  }
  const len = binaryString.length;
  const bytes = new Uint8Array(len);
  for (let i = 0; i < len; i++) {
    const ascii = binaryString.charCodeAt(i);
    bytes[i] = ascii;
  }
  return bytes.buffer;
};
